<?php
namespace DRY\Structure;

/**
 * DonReY Framework 2012 :: List-type data structure
 * Organizes data into ordered lists, which can be accessed abitrarely, as a queue ( FiFo ) or as stack ( LiFo ).
 *   Reads/Navigation follow the ordering inside the structure.
 * Also implements Iterator, for reading the elements one by one in a foreach(), however it runs a query on EACH item!
 *   (useful however when list elements are complex data which cannot be read in joined queries, and must be read individually anyway - example: "Multi" record)
 *   To read multiple items at once with one query, use read_All() or slice() which return an array of items and their positions.
 * NOTE: that this type of structure behaves very similar to PHP's array_functions, it even has most of the same names, however it works on a database, not in memory
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/Structure/list.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Structure
 */
class Type_List extends base implements \Countable, \ArrayAccess, \Iterator
{

	/**
	 * Current list size, a cache of the count() operation
	 * @var int
	 */
	protected $size;

	/**
	 * Current position in the list - Various operations affect this value, while read operations might start from the pointer
	 * @var int
	 */
	protected $position = 1;

	/**
	 * ID of the last item in the list (the highest "position")
	 * @var int
	 */
	protected $last_item_id;

//===== Read Methods =====//

	/**
	 * Determines the current size of the list ( and caches the information internally )
	 *
	 * @return int List size
	 */
	public function count()
	{
		if(!is_null($this-> size))
			return $this-> size;

		// Measures the list by location it's highest position member
		$q = $this->_structure['queryfactory']-> select()
			-> table($this->_table)
			-> where("structure_id = ?", $this->_id)
			-> order("position", "DESC")
			-> limit(1);

		$info = $this->_structure['connection']-> query($q)-> current();

		if($info) {
			$this-> last_item_id = $info['item_id'];
			// the actual number of elements in the list
			$this-> size = (int)$info['position'];
			return $this-> size;
		} else
			// currently no elements in the list
			return 0;
	}

	/**
	 * Resets the current list position to first element
	 */
	public function reset()
	{
		$this-> position = 1;
	}

	/**
	 * Alias of self::reset()
	 */
	public function rewind()
	{
		$this-> position = 1;
	}

	/**
	 * Checks if there are any more items to read
	 */
	public function valid()
	{
		$this-> count();
		if(!is_null($this-> position) && $this-> position <= $this-> size)
			return true;
		return false;
	}

	/**
	 * Reads the item_id at the current list pointer or prepares the query in joined mode and can also move the position pointer to another arbitrary position
	 *
	 * @param int $position Optional, specifies a new position
	 * @return bool|array|\DRY\Queryfactory\Dialect_base
	 */
	public function current($position = null)
	{
		if(!is_null($position))
			$this-> position = $position;

		// size and position
		$this-> count();
		if($this-> size == 0 || is_null($this-> position) || $this-> position > $this-> size) {
			$this-> position = null;
			return false; // error, position out of bounds or no items in the list
		}

		// reading query
		$q = $this->_structure['queryfactory']-> select()
			-> where("_struct_.structure_id = ?", $this->_id)
			-> where('_struct_.position = ?', $this-> position)
			-> limit(1);

		if($this-> joinmode) { // joined mode
			$q-> table(array($this->_table, "_struct_"), \DRY\Module\Queryfactory::autoalias_Add_Prefix("item_id,position", '_list_'));
			return $q;
		} else { // actual read
			$q-> table(array($this->_table, "_struct_"), 'item_id,position');
			return $this->_structure['connection']-> query($q)-> current();
		}
	}

	/**
	 * Adds the required elements to a data result object so the list information ( position ) can be sepparated from the data itself
	 *
	 * @param \DRY\Database\Driver_result_base $res
	 */
	public static function prepare_Result(\DRY\Database\Driver_result_base $res, $index_field = null)
	{
		$res-> split_by_prefixes['__LIST'] = '_list_';
		$res-> index_field = $index_field;
	}

	/**
	 * Returns the position during a read
	 */
	public function key()
	{
		return $this-> position;
	}

	/**
	 * Skips an element in the list, next current() call will use the one after
	 * @param bool $loop If True, when going beyond the end of list, position goes back to first item
	 * @return bool True if reached the end
	 */
	public function next($loop = false)
	{
		$this-> count();
		if(++$this-> position > $this-> size) {
			if($loop)
				$this-> position = 1;
			else
				$this-> position = null; // next read will fail if the position is not reset
			return false;
		}
		return true;
	}

	/**
	 * Resets the current list position to last element
	 */
	public function end()
	{
		$this-> count();
		$this-> position = $this-> size;
	}

	/**
	 * Goes to the previous element
	 * @param bool $loop If True, when going beyond the start of list, position goes to the last element
	 * @return bool True if reached the beginning
	 */
	public function prev($loop = false)
	{
		if(--$this-> position < 1) {
			if($loop)
				$this-> end();
			else
				$this-> position = null; // next read will fail if the position is not reset
			return false;
		}
		return true;
	}

	/**
	 * Make some calculations for range-select methods
	 * @param int $position
	 * @param int $length
	 * @param int $end
	 * @param bool $r_null
	 */
	private function prepare_Position(&$position, &$length, &$end = null, $r_null = true)
	{
		$this-> count();

		// position check
		if($position < 0) {
			$position = $this-> size + $position + 1;
			if($position < -$this-> size)
				return false; // out of bounds
		} elseif(($this-> size > 0 && $position > $this-> size) || $position==0)
			return false; // out of bounds

		if($this-> size==0)
			$position = 1;

		// length check
		if($length < 0) {
			$end = $this-> size + $length;
			if($end < $position) {
				$length = 0;
				$end = null; // nothing to select
			}
		} elseif($length == 0) {
			if($r_null)
				$end = $this-> size; // when it doesn't, consider length 0 = full list
			else
				$end = null; // when $replacement exists, nothing will get removed
		} else {
			$end = $position + $length - 1;
			if($end > $this-> size)
				$end = $this-> size;
		}
		return true;
	}

	/**
	 * Reads the entire list, ordered, with no position restrictions.
	 * Should be a bit faster than slice(), since it doesn't run an additional query to determine the list size.
	 *
	 * @return \DRY\Database\Driver_result_base|\DRY\Queryfactory\Dialect_base
	 */
	public function read_All()
	{
		$q = $this->_structure['queryfactory']-> select()
			-> where("_struct_.structure_id = ?", $this->_id)
			-> order('_struct_.position');

		if($this-> joinmode) { // joined mode (returns Queryfactory object)
			$q-> table(array($this->_table, "_struct_"), \DRY\Module\Queryfactory::autoalias_Add_Prefix("item_id,position", '_list_'));
			return $q;
		} else { // actual read (returns Result object)
			$q-> table(array($this->_table, "_struct_"), 'item_id,position');
			return $this->_structure['connection']-> query($q);
		}
	}

	/**
	 * Reads an ordered slice of the list - returns either an array with item_id's, or a query which will read the item themselves
	 *
	 * @param int $position First item in the returned list
	 * @param int $length How many items to read starting with $position, but when 0 it will return all starting with $position
	 * @return null|bool|\DRY\Database\Driver_result_base|\DRY\Queryfactory\Dialect_base
	 */
	public function slice($position = 1, $length = 0)
	{
		$end = null;
		if($this-> prepare_Position($position, $length, $end)===false)
			return false; // out of bounds

		if($this-> size==0)
			return null; // empty list

		if(!is_null($end)) {
			// reading query
			$q = $this->_structure['queryfactory']-> select()
				-> where("_struct_.structure_id = ?", $this->_id)
				-> where('_struct_.position BETWEEN ? AND ?', (int)$position, (int)$end)
				-> order('_struct_.position');

			if($this-> joinmode) { // joined mode
				$q-> table(array($this->_table, "_struct_"), \DRY\Module\Queryfactory::autoalias_Add_Prefix("item_id,position", '_list_'));
				return $q;
			} else { // actual read
				$q-> table(array($this->_table, "_struct_"), 'item_id,position');
				return $this->_structure['connection']-> query($q);
			}
		}
		return null;
	}

	/**
	 * Returns the position of an item_id in the list
	 * @param int $item_id
	 * @return int|null Position or nothing if item_id not part of the list.
	 */
	public function search_Item_Id($item_id)
	{
		$q = $this->_structure['queryfactory']-> select()
			-> table($this->_table, 'position')
			-> where("structure_id = ?", $this->_id)
			-> where("item_id = ?", $item_id)
			-> limit(1);
		$row = $this->_structure['connection']-> query($q)-> current();
		if($row)
			return $row['position'];
	}

//===== Basic Change/Write Methods =====//

	/**
	 * Similar to array_splice(), depending on position, length and replacement, this method performs a read and/or delete and/or insert
	 * Note that when used in joined mode, it creates a temporary table and the alias "_struct_" will be that table, not the original structure, since it modifies the original table.
	 *  - the temporary table's name is "List_Splice_extract", and it can be removed easy by calling splice_Clean()
	 * @param int $position
	 * @param int $length
	 * @param mixed $replacement Array with item_id's or multiple item ID's as parameters, will get inserted/replaced at position
	 * @param bool $no_read Disables reading/returning of data, when this method is used internally (or when only used to cut a part of a list without returning anything)
	 * @return null|bool|\DRY\Database\Driver_result_base|\DRY\Queryfactory\Dialect_base
	 *
	 * @todo Currently, it doesn't support appending to the list using $position > $this-> size ... fix that, somehow?
	 */
	public function splice($position = 1, $length = 0, $replacement = null, $no_read = false)
	{
		$end = null;
		if($this-> prepare_Position($position, $length, $end, is_null($replacement))===false)
			return false; // out of bounds

		$qf = $this->_structure['queryfactory'];
		$db = $this->_structure['connection'];
		$ret = null;
		// read and cut, when there is a start (position) and an end
		if(!is_null($end)) {

			if(!$no_read) {
				if($this-> joinmode) { // joined_mode
					// create the temp table
					$db-> query($qf-> create('table')
						-> flags('temporary', 'MEMORY')
						-> name("__List_extract")
						-> column("item_id")
						-> column("position", "int unsigned")
						-> index("index", "position")
					);

					// insert...select into the temp table
					$db-> query($qf-> insert()
						-> table("__List_extract")
						-> values($qf-> select()
							-> table($this->_table, 'item_id,position')
							-> where("structure_id = ?", $this->_id)
							-> where('position BETWEEN ? AND ?', (int)$position, (int)$end)
							-> order('position')
						)
					);
				}

				// prepares primary read query
				$q = $qf-> select()
					-> order('_struct_.position');

				if(!$this-> joinmode) {
					$q
						-> where("_struct_.structure_id = ?", $this->_id)
						-> where('_struct_.position BETWEEN ? AND ?', (int)$position, (int)$end);
				}

				if($this-> joinmode) { // joined mode
					$q-> table(array("__List_extract", "_struct_"), \DRY\Module\Queryfactory::autoalias_Add_Prefix("item_id,position", '_list_'));
					$ret = $q;
				} else { // actual read
					$q-> table(array($this->_table, "_struct_"), 'item_id,position');
					$ret = $db-> query($q);
				}
			}

			$db-> query("LOCK TABLES {$this->_table} WRITE");
			$lock = true;

			// finally, cut the extracted part
			// TODO: Support join-mode for delete()? - possibly when not returning data
			$db-> query($qf-> delete()
				-> table($this->_table)
				-> where('position BETWEEN ? AND ?', (int)$position, (int)$end)
			);
			$length = $end - $position + 1;
		}
		// freeze _table while doing update/insert (to prevent another thread to modify the list while working with it and messing the update values)
		if(!isset($lock))
			$db-> query("LOCK TABLES {$this->_table} WRITE");

		// fix the length of the list
		if(is_null($replacement)) {
			$sd = -$length;
		} else {
			$replacement = (array)$replacement;
			// calculate the size difference between what was extracted and what is inserted, to change the length of the list and the positions
			$sd = count($replacement) - $length;
		}

		if($sd > 0 && $end < $this-> size) {
			// increase "position" for all nodes starting from $end + 1 (or $position if there's no $end)
			$q = $qf-> update()
				-> table($this->_table)
				-> where("structure_id = ?", $this->_id)
				-> values(array("position"=>"position + $sd"), null, true)
				-> order('position', "DESC");
			if(!is_null($end))
				$q-> where('position > ?', $end);
			else
				$q-> where('position >= ?', $position);
			$db-> query($q);
		} elseif($sd < 0 && $end < $this-> size) {
			// decrease "position for all nodes starting from $end + 1 (or $position if there's no $end)
			$q = $qf-> update()
				-> table($this->_table)
				-> where("structure_id = ?", $this->_id)
				-> values(array("position"=>"position - ".(-$sd)), null, true)
				-> order('position', "ASC");
			if(!is_null($end))
				$q-> where('position > ?', $end);
			else
				$q-> where('position >= ?', $position);
			$db-> query($q);
		}

		// insert replacement (if it's the case)
		if(!is_null($replacement)) {
			$q = $qf-> insert()
				-> table($this->_table);
			foreach($replacement as $k=>$item_id)
				$q-> values(array(
					'structure_id'=> $this->_id,
					'item_id'=> $item_id,
					'position'=> $k+$position
				));
			$db-> query($q);
		}
		$db-> query("UNLOCK TABLES");
		return $ret;
	}

	/**
	 * Removes the temporary table that might have been created by splice(), pop(), shift() or other methods that affect the list while also returning a result
	 */
	public function temp_Clean()
	{
		$q = $this->_structure['queryfactory']-> drop('table')
			-> name("__List_extract");
		$this->_structure['connection']-> query($q);
	}

	/**
	 * Adds one ore more item_id's to the list (at the end), lengthening the list
	 * @param int $item_id, $idem_id, ...
	 */
	public function push()
	{
		$this-> count();
		$q = $this->_structure['queryfactory']-> insert()
			-> table($this->_table);
		$inserts = func_get_args();
		foreach($inserts as $k=>$item_id)
			$q-> values(array(
				'structure_id'=> $this->_id,
				'item_id'=> $item_id,
				'position'=> $this-> size + $k + 1
			));
		$this->_structure['connection']-> query($q);
		$this-> size+= func_num_args();
		return $this-> size;
	}

	/**
	 * Injector: Reads the last item_id in the list (or the item itself with injection) and then removes it from the list
	 */
	public function pop()
	{
		return $this-> splice(-1);
	}

	/**
	 * Adds one or more item_id's in the beginning of the list
	 * @param int $item_id, $item_id, ...
	 */
	public function unshift()
	{
		$inserts = func_get_args();
		$this-> splice(1, 0, $inserts);
		$this-> size+= func_num_args();
		return $this-> size;
	}

	/**
	 * Injector: Reads the first item_id in the list (or the item itself with injection) and then removes it from the list
	 */
	public function shift()
	{
		return $this-> splice(1, 1);
	}

	/**
	 * Removes the entire structure data
	 * @see DRY\Structure\base::destroy()
	 */
	public function destroy()
	{
		// TODO: Support joined mode deletion ( so it deletes the data too if necessary )
		$q = $this->_structure['queryfactory']-> delete()
			-> table($this->_table)
			-> where("structure_id = ?", $this->_id);
		$this->_structure['queryfactory']-> query($q);

		$this-> last_item_id = null;
		return $this->_structure['queryfactory']-> affected_rows;
	}

	/**
	 * Combines multiple lists into one, appending all the other lists to this one, while keeping item_id's unique (does not repeat item_id's)
	 * Use only if many lists refer to the same table of items!
	 * @param Data_Structure_List $list1, $list2, ....
	 */
	public function merge()
	{

	}

	/**
	 * Inverts the order of a portion of the list
	 * - actually, loads that portion in memory, deletes it and reinserts it in reverse order in one query
	 */
	public function reverse($position = 1, $length = 0)
	{
		// calls reorder() after generating the $new_positions
	}

/*=====
 * ArrayAccess Implementation - Allows the list to behave just like a real PHP array (with some twists)
 * $struct[$position] = $item_id - Sets the item_id at position (changes which item is in that position), or inserts a new item at the end (when $position == null )
 * $item_id = $struct[$position] - Reads the item_id at position, or the queryfactory object which will read the item itself when joined
 * unset($struct[$item_id]) - Removes the item_id from the list, or removes the item itself (in join mode)
 * isset($struct[$item_id]) - Checks if the item_id exists in the list
 =====*/

	/**
	 * Injector: Reads the item itself at position
	 * @param int $offset
	 */
	public function offsetGet($offset)
	{
		return $this-> current($offset);
	}

	/**
	 * Sets the item_id at position (changes which item is in that position).
	 * Position must already exist, or if none specified ( $this[] = $value ), it uses self::push() to insert a new item in the list
	 * @param int $offset
	 * @param int $value
	 */
	public function offsetSet($offset, $value)
	{
		if(is_null($offset))
			$this-> push($value);
		else {
			$q = $this->_structure['queryfactory']-> update()
				-> table($this->_table)
				-> where("structure_id = ?", $this->_id)
				-> where("position = ?", $offset)
				-> values(array(
					'item_id'=> $value
				));
			$this->_structure['connection']-> query($q);
		}
	}

	/**
	 * Checks if the item_id exists in the list
	 * @param int $item_id
	 */
	public function offsetExists($item_id)
	{
		return !is_null($this-> search_Item_Id($item_id));
	}

	/**
	 * Injector: Removes the item_id from the list (if it exists), or removes the item itself (with injection)
	 * @param int $item_id
	 */
	public function offsetUnset($item_id)
	{
		$item_position = $this-> search_Item_Id($item_id);
		if($item_position)
			return $this-> splice($item_position, 1, null, true);
	}

//===== Speciality Methods (which do not necesarely imitate PHP arrays) =====//

	/**
	 * Creates the list from an array of item_id's
	 * @param array[int] $item_ids
	 */
	public function create_From($item_ids)
	{
		$this-> notify("list:create_From", $item_ids);
	}

	/**
	 * Reorders the list by moving an element to another position
	 * By default, it moves one position. If already at minimum position (1) or maximum position,
	 *   depending on the movement type, no operation is done, but the function returns true (success)
	 *
	 * @param int $position
	 * @param string|int $where Can be 'first','left','right','last' or new position ( for example, move(4, 2) on a size-5 list will move elements like this: 1, 4, 2, 3, 5 - numbers are old positions)
	 * @param int $distance Makes sense for when $where is string: first/last = distance from margins (1 = exactly there, 2 = second from begin/end), left/right - how much is moved
	 */
	public function move($position, $where, $distance = 1)
	{

	}

	/**
	 * Changes the order of a slice of the list by using a reorder map array. This method can be used to sort the list (after the new locations have been determined externally)
	 * This is actually done by reading that portion of the list, removing it then reinserting elements in the list in one query
	 * @param int $position
	 * @param int $length
	 * @param array $new_positions New locations of old positions. Example: reorder(3, 5, array(5,4,3,7,6)) will reverse first three and last two of a length-5 slice, starting with the third element
	 */
	public function reorder($position = 1, $length = 0, $new_positions = array())
	{

	}

}